﻿/* 
 * Copyright (c) 2009-2013, Andriy Syrov
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided 
 * that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the 
 * following disclaimer.
 * 
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 
 * the following disclaimer in the documentation and/or other materials provided with the distribution.
 *
 * Neither the name of Andriy Syrov nor the names of his contributors may be used to endorse or promote 
 * products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY 
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 *   
 */

namespace Codeplex.InertialScroll
{
    using System;
    using System.Diagnostics;

    /// 
    /// <summary>
    /// Inertial scroll mode</summary>
    /// 
    public enum InertialMode
    {
        Finished,        // scrolling is finished 
        Scrolling,       // inertial scrolling inside viewport
        EasingOut,       // moving in direction outside of visible viewport because of inertia
        EasingIn         // returning to visible viewport after bouncing off elastic margin    
    }

    /// 
    /// <summary>
    /// State of inertial scroll</summary>
    /// 
    public struct ScrollState
    {
        public double                                   Speed;
        public double                                   Position;
        public double                                   EasingTime;
        public InertialMode                             Mode;

        public ScrollState(
            double                                      initialSpeed,
            double                                      initialPosition
        )
        {
            Speed           = Position = EasingTime = 0;
            Mode            = InertialMode.Finished;

            Set(initialSpeed, initialPosition);
        }

        public void Set(
            double                                      initialSpeed,
            double                                      initialPosition
        )
        {
            Debug.Assert(!double.IsNaN(initialSpeed));
            Debug.Assert(!double.IsNaN(initialPosition));

            Speed    = initialSpeed;
            Position = initialPosition;

            EasingTime = 0;
            Mode       = Speed != 0 ? InertialMode.Scrolling: InertialMode.Finished;
        }

        public void SetSpeed(
            double                                      p1,
            double                                      p0,
            DateTime                                    t1,
            DateTime                                    t0
        )
        {
            if (t1 > t0)
            {
                Speed = -(p1 - p0) / (t1 - t0).TotalMilliseconds;
            }
            else
            {
                Speed = 0;
            }
        }

        public void Finish(
            double                                      size,
            double                                      viewport,
            double                                      position
        )
        {
            Mode       = InertialMode.Finished;
            Speed      = 0;
            EasingTime = 0;

            if (position < 0)
            {
                Position = 0;
            }
            else if (position > size - viewport)
            {
                Position = size - viewport;
            }
            else
            { 
                Position = position;
            }
        }
    }

    /// 
    /// <summary>
    /// Inertial scroll calculator</summary>
    /// 
    public class InertialScroll
    {
        public const double                              INERTIAL_ACCELERATION = 0.002f;     // inertial acceleration 
        public const double                              MARGIN_TIME           = 750.0f;     // milliseconds for margin easing in/out

        public virtual ScrollState Calculate(
            double                                       timeDelta,
            ScrollState                                  state,
            double                                       size,
            double                                       viewport
        )
        {
            //
            // fix state according to state, size and viewport
            //
            state = FixEasingMode(state, size, viewport);

            switch (state.Mode)
            {
                case InertialMode.Scrolling:
                    state = CalculateScroll(timeDelta, state, size, viewport);
                    break;

                case InertialMode.EasingOut:
                    state = CalculateEasingOut(timeDelta, state, size, viewport);
                    break;

                case InertialMode.EasingIn:
                    state = CalculateEasingIn(timeDelta, state, size, viewport);
                    break;
            }
            return state;
        }

        #region protected

        /// 
        /// <summary>
        /// Fix mode and speed to return to viewport if we are outside of visible area and 
        /// parameters are not set correctly for passed size and viewport</summary>
        /// 
        protected virtual ScrollState FixEasingMode(
            ScrollState                                  state,
            double                                       size,
            double                                       viewport
        )
        {
            if ((state.Mode == InertialMode.Scrolling || state.Mode == InertialMode.Finished) && (state.Position < 0 || state.Position + viewport > size))
            {
                state.Mode = InertialMode.EasingIn;
                if (state.Position > 0)
                {
                    state.Speed = 2 * ((size - viewport) - state.Position) / (MARGIN_TIME / 2);
                }
                else
                {
                    state.Speed = -(2 * state.Position) / (MARGIN_TIME / 2);
                }
                state.EasingTime = 0;
            }
            return state;
        }

        /// 
        /// <summary>
        /// Calculate next step for passed time delta in easing out mode</summary>
        /// 
        protected virtual ScrollState CalculateEasingOut(
            double                                       timeDelta,
            ScrollState                                  state,
            double                                       size,
            double                                       viewport
        )
        {
            double                                       acceleration;

            if (timeDelta + state.EasingTime <= MARGIN_TIME / 2)
            {
                acceleration     = AccCalc.CalculateAcceleration(state.Speed, MARGIN_TIME / 2 - state.EasingTime);
                state.Position   = AccCalc.CalculatePosition(state.Position, acceleration, state.Speed, timeDelta);
                state.Speed      = AccCalc.CalculateSpeed(acceleration, timeDelta, state.Speed); 
                state.EasingTime += timeDelta;
            }
            else
            {
                state      = CalculateEasingOut(MARGIN_TIME / 2 - state.EasingTime, state, size, viewport);

                state.Mode = InertialMode.Finished;
                timeDelta  = timeDelta - (MARGIN_TIME / 2 - state.EasingTime);
                state      = FixEasingMode(state, size, viewport);
                state      = Calculate(timeDelta, state, size, viewport);
            }
            return state;
        }

        /// 
        /// <summary>
        /// Calculate next step for passed time delta in easing in mode</summary>
        /// 
        protected virtual ScrollState CalculateEasingIn(
            double                                       timeDelta,
            ScrollState                                  state,
            double                                       size,
            double                                       viewport
        )
        {
            double                                       acceleration;

            if (timeDelta + state.EasingTime < MARGIN_TIME / 2)
            {
                if (state.Position < size - viewport)
                {
                    acceleration = AccCalc.GetStopAcceleration(-state.Position, state.Speed, MARGIN_TIME / 2 - state.EasingTime);
                }
                else
                {
                    acceleration = AccCalc.GetStopAcceleration((size - viewport) - state.Position, state.Speed, MARGIN_TIME / 2 - state.EasingTime);
                }

                state.Position   = AccCalc.CalculatePosition(state.Position, acceleration, state.Speed, timeDelta);
                state.Speed      = AccCalc.CalculateSpeed(acceleration, timeDelta, state.Speed); 
                state.EasingTime += timeDelta;
            }
            else
            {
                state.Finish(size, viewport, state.Position);
            }
            return state;
        }

        /// 
        /// <summary>
        /// Calculate next step for passed time delta for inertial scroll mode</summary>
        /// 
        protected virtual ScrollState CalculateScroll(
            double                                       timeDelta,
            ScrollState                                  state,
            double                                       size,
            double                                       viewport
        )
        {
            double acceleration = state.Speed > 0 ? -INERTIAL_ACCELERATION : INERTIAL_ACCELERATION;
            double fullStopTime = -state.Speed / acceleration;
                        
            if (fullStopTime < timeDelta)
            {
                timeDelta = fullStopTime;
                state.Mode = InertialMode.Finished;
            }
            double position = AccCalc.CalculatePosition(state.Position, acceleration, state.Speed, timeDelta);

            if (position < 0)
            {
                double time0 = AccCalc.CalculateTime(state.Speed, acceleration, state.Position);

                if (time0 >= MARGIN_TIME)
                {
                    state.Finish(size, viewport, 0.0);
                }
                else 
                {
                    double time = Math.Max(0, timeDelta - time0);

                    state.Mode         = InertialMode.EasingOut;
                    state.Position     = 0;
                    state.Speed        = AccCalc.CalculateSpeed(acceleration, time0, state.Speed);
                    state.EasingTime   = 0;
                    state = Calculate(timeDelta - time0, state, size, viewport);
                }
            }
            else if (position > size - viewport)
            {
                double time0 = AccCalc.CalculateTime(state.Speed, acceleration, -(size - (state.Position + viewport)));

                if (time0 >= MARGIN_TIME)
                {
                    state.Finish(size, viewport, size - viewport);
                }
                else 
                {
                    state.Mode         = InertialMode.EasingOut;
                    state.Position     = size - viewport;
                    state.Speed        = AccCalc.CalculateSpeed(acceleration, time0, state.Speed);
                    state.EasingTime   = 0;
                    state = Calculate(timeDelta - time0, state, size, viewport);
                }
            }
            else
            {
                state.Speed = state.Mode == InertialMode.Finished ? 0 : AccCalc.CalculateSpeed(acceleration, timeDelta, state.Speed);
                state.Position = position;
            }
            return state;
        }

        #endregion 
    }
}

